package cn.sciento.export;


import cn.sciento.core.domain.Page;
import cn.sciento.core.exception.CommonException;
import cn.sciento.core.oauth.CustomUserDetails;
import cn.sciento.core.oauth.DetailsHelper;
import io.choerodon.mybatis.pagehelper.domain.PageRequest;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections4.CollectionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import cn.sciento.core.async.TaskDTO;
import cn.sciento.core.export.ExportAsyncTemplate;
import cn.sciento.core.export.ExportTaskDTO;
import cn.sciento.export.annotation.ExcelExport;
import cn.sciento.export.util.ResponseWriter;
import cn.sciento.export.vo.ExportColumn;
import cn.sciento.export.vo.ExportParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

public class ExportDataHelper {
    private static final Logger LOGGER = LoggerFactory.getLogger(ExportDataHelper.class);

    private static final String EXPORT_PARAM_MUST_NOT_BE_NULL = "ExportParam must not be null.";

    private ExportColumnHelper exportColumnHelper;

    private ExecutorService executorService;

    private ExportFutureManager futureManager;

    @Autowired(required = false)
    private ExportAsyncTemplate exportAsyncTemplate;

    @Autowired
    private ExportProperties properties;

    @Value("${spring.application.name:}")
    private String serviceName;

    @Value("${management.server.port:8081}")
    private String port;

    @Value("${server.hostname:}")
    private String hostname;

    public ExportDataHelper() {
    }

    public ExportDataHelper(ExportColumnHelper exportColumnHelper) {
        this.exportColumnHelper = exportColumnHelper;
    }

    public ExportDataHelper(ExportColumnHelper exportColumnHelper, ExecutorService executorService, ExportFutureManager futureManager) {
        this.exportColumnHelper = exportColumnHelper;
        this.executorService = executorService;
        this.futureManager = futureManager;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = {RuntimeException.class})
    public void exportExcel(ProceedingJoinPoint joinPoint, ExcelExport excelExport, HttpServletResponse response) throws Exception {
        Object target = joinPoint.getTarget();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        Object[] args = joinPoint.getArgs();
        PageRequest pageRequest = null;
        ExportParam exportParam = null;
        for (Object arg : args) {
            if (arg instanceof PageRequest) {
                pageRequest = (PageRequest) arg;
            } else if (arg instanceof ExportParam) {
                exportParam = (ExportParam) arg;
            }
        }
        if (exportParam == null) {
            LOGGER.error("ExportParam must not be null.");
            throw new IllegalArgumentException("ExportParam must not be null.");
        }
        ExportColumn root = this.exportColumnHelper.getCheckedExportColumn(excelExport, Optional.<Set<Long>>ofNullable(exportParam.getIds()).orElse(Collections.emptySet()));
        if (!root.isChecked())
            throw new CommonException("export.column.least-one");
        Set<String> selection = this.exportColumnHelper.getSelection(excelExport, exportParam.getIds());
        exportParam.setSelection(selection);
        boolean paging = (pageRequest != null);
        int singleSheetMaxRow = ((exportParam.getSingleSheetMaxRow() == null) ? this.properties.getSingleSheetMaxRow() : exportParam.getSingleSheetMaxRow()).intValue();
        int singleExcelMaxSheetNum = ((exportParam.getSingleExcelMaxSheetNum() == null) ? this.properties.getSingleExcelMaxSheetNum() : exportParam.getSingleExcelMaxSheetNum()).intValue();
        ExcelExporter excelExporter = new ExcelExporter(root, exportParam.getFillerType(), singleExcelMaxSheetNum, singleSheetMaxRow, response);
        excelExporter.setFileName(exportParam.getFileName());
        String fileName = excelExporter.getFileName();
        CustomUserDetails userDetails = DetailsHelper.getUserDetails();
        if (paging) {
            pageRequest.setSize(root.getExcelSheet().pageSize());
            if (exportParam.isAsync()) {
                ExportTaskDTO dto = constructExportTaskDto(fileName, getLocalhost(), this.serviceName);
                String uuid = asyncExecute(new ExportTask(target, method, args, pageRequest, excelExporter, this.exportAsyncTemplate, dto, () -> DetailsHelper.setCustomUserDetails(userDetails)));
                ResponseWriter.writeAsyncRequestSuccess(response, uuid);
            } else {
                try {
                    requestDataAndFillSheet(target, method, args, pageRequest, excelExporter);
                } catch (Throwable e) {
                    excelExporter.setErrorMessage(e.getMessage());
                }
                excelExporter.downloadExcel();
            }
        } else if (exportParam.isAsync()) {
            ExportTaskDTO dto = constructExportTaskDto(fileName, getLocalhost(), this.serviceName);
            String uuid = asyncExecute(new ExportTask(target, method, args, excelExporter, this.exportAsyncTemplate, dto, () -> DetailsHelper.setCustomUserDetails(userDetails)));
            ResponseWriter.writeAsyncRequestSuccess(response, uuid);
        } else {
            try {
                requestDataAndFillSheet(target, method, args, excelExporter);
            } catch (Throwable e) {
                excelExporter.setErrorMessage(e.getMessage());
            }
            excelExporter.downloadExcel();
        }
    }

    private String getLocalhost() {
        if ("".equals(this.hostname)) {
            String hostname;
            try {
                hostname = Inet4Address.getLocalHost().getHostAddress();
            } catch (UnknownHostException e) {
                throw new RuntimeException("Export task execute failed", e);
            }
            return hostname + ":" + this.port;
        }
        return this.hostname + ":" + this.port;
    }

    private ExportTaskDTO constructExportTaskDto(String taskName, String hostName, String serviceName) {
        ExportTaskDTO dto = new ExportTaskDTO();
        dto.setTaskName(taskName);
        dto.setHostName(hostName);
        dto.setServiceName(serviceName);
        dto.setTenantId(Long.valueOf(0L));
        dto.setUserId(Long.valueOf(0L));
        CustomUserDetails userDetails = DetailsHelper.getUserDetails();
        if (userDetails != null) {
            Long tenantId = userDetails.getTenantId();
            Long userId = userDetails.getUserId();
            if (tenantId != null)
                dto.setTenantId(tenantId);
            if (userId != null)
                dto.setUserId(userId);
        }
        return dto;
    }

    private String asyncExecute(ExportTask exportTask) {
        if (this.exportAsyncTemplate == null)
            throw new IllegalStateException("请确保已引入异步支持组件 [cn.sciento.core.export.ExportAsyncTemplate].");
        UUID uuid = UUID.randomUUID();
        exportTask.getDto().setTaskCode(uuid.toString());
        return this.exportAsyncTemplate.submit(exportTask.getDto(), () -> {
            Future<?> future = this.executorService.submit(exportTask);
            this.futureManager.put(uuid, future);
            return uuid.toString();
        });
    }

    @FunctionalInterface
    public static interface Initialization {
        void init();
    }

    public class ExportTask implements Runnable {
        private Object target;

        private Method method;

        private Object[] args;

        private PageRequest pageRequest;

        private ExcelExporter excelExporter;

        private ExportAsyncTemplate exportAsyncTemplate;

        private ExportTaskDTO dto;

        private Initialization initialization;

        ExportTask(Object target, Method method, Object[] args, PageRequest pageRequest, ExcelExporter excelExporter, ExportAsyncTemplate exportAsyncTemplate, ExportTaskDTO dto, Initialization initialization) {
            this.target = target;
            this.method = method;
            this.args = args;
            this.pageRequest = pageRequest;
            this.excelExporter = excelExporter;
            this.exportAsyncTemplate = exportAsyncTemplate;
            this.dto = dto;
            this.initialization = initialization;
        }

        ExportTask(Object target, Method method, Object[] args, ExcelExporter excelExporter, ExportAsyncTemplate exportAsyncTemplate, ExportTaskDTO dto, Initialization initialization) {
            this(target, method, args, null, excelExporter, exportAsyncTemplate, dto, initialization);
        }

        public void run() {
            this.initialization.init();
            try {
                if (this.pageRequest == null) {
                    ExportDataHelper.this.requestDataAndFillSheet(this.target, this.method, this.args, this.excelExporter);
                } else {
                    ExportDataHelper.this.requestDataAndFillSheet(this.target, this.method, this.args, this.pageRequest, this.excelExporter);
                }
                byte[] file = this.excelExporter.readFileBytes();
                String fileType = this.excelExporter.isZip() ? "zip" : "excel";
                Map<String, Object> additionInfo = new HashMap<>(2);
                additionInfo.put("fileType", fileType);
                additionInfo.put("file", file);
                this.exportAsyncTemplate.doWhenFinish(this.dto, additionInfo);
            } catch (Throwable e) {
                ExportDataHelper.LOGGER.error("export task execute error", e);
                this.exportAsyncTemplate.doWhenOccurException(this.dto, e);
            } finally {
                try {
                    this.excelExporter.cleanTempFile();
                } catch (IOException e) {
                    ExportDataHelper.LOGGER.error("The temp file clean failed.", e);
                }
                ExportDataHelper.this.futureManager.remove(this.dto.getTaskCode());
            }
        }

        public ExportTaskDTO getDto() {
            return this.dto;
        }
    }

    public void exportTemplate(ProceedingJoinPoint joinPoint, ExcelExport excelExport, HttpServletResponse response) throws Exception {
        Object[] args = joinPoint.getArgs();
        ExportParam exportParam = null;
        for (Object arg : args) {
            if (arg instanceof ExportParam)
                exportParam = (ExportParam) arg;
        }
        if (exportParam == null) {
            LOGGER.warn("ExportParam must not be null.");
            throw new IllegalArgumentException("ExportParam must not be null.");
        }
        ExportColumn root = this.exportColumnHelper.getCheckedExportColumn(excelExport,
                Optional.<Set<Long>>ofNullable(exportParam.getIds()).orElse(Collections.emptySet()));
        if (!root.isChecked())
            throw new CommonException("export.column.least-one");
        int singleSheetMaxRow = ((exportParam.getSingleSheetMaxRow() == null) ? this.properties.getSingleSheetMaxRow() : exportParam.getSingleSheetMaxRow());
        int singleExcelMaxSheetNum = ((exportParam.getSingleExcelMaxSheetNum() == null) ? this.properties.getSingleExcelMaxSheetNum() : exportParam.getSingleExcelMaxSheetNum());
        ExcelExporter excelExporter = new ExcelExporter(root, exportParam.getFillerType(), singleExcelMaxSheetNum, singleSheetMaxRow, response);
        excelExporter.fillTitle();
        excelExporter.downloadExcel();
    }

    private void requestDataAndFillSheet(Object target, Method method, Object[] args, PageRequest pageRequest, ExcelExporter excelExporter) throws InvocationTargetException, IllegalAccessException {
        Object result = method.invoke(target, args);
        Page pageData = null;
        if (result instanceof Page) {
            pageData = (Page) result;
        } else if (result instanceof ResponseEntity) {
            ResponseEntity responseEntity = (ResponseEntity) result;
            if (responseEntity.getBody() instanceof Page)
                pageData = (Page) responseEntity.getBody();
        }
        if (pageData == null) {
            String message = "Response data type must be [cn.sciento.core.domain.Page] or ResponseEntity<Page>";
            LOGGER.warn(message);
            throw new IllegalArgumentException(message);
        }
        long totalElements = pageData.getTotalElements();
        if (totalElements < 1L) {
            excelExporter.fillTitle();
            return;
        }
        if (totalElements > 1000000L)
            throw new CommonException("export.too-many-data");
        excelExporter.fillSheetForPage(pageData.getContent());
        if (pageData.getContent() != null)
            pageData.getContent().clear();
        if (pageRequest.getPage() < pageData.getTotalPages() - 1) {
            pageRequest.setPage(pageRequest.getPage() + 1);
            requestDataAndFillSheet(target, method, args, pageRequest, excelExporter);
        }
    }

    private void requestDataAndFillSheet(Object target, Method method, Object[] args, ExcelExporter excelExporter) throws InvocationTargetException, IllegalAccessException {
        Object result = method.invoke(target, args);
        List<?> data = null;
        if (result instanceof List) {
            data = (List) result;
        } else if (result instanceof ResponseEntity) {
            ResponseEntity responseEntity = (ResponseEntity) result;
            if (responseEntity.getBody() instanceof List)
                data = (List) responseEntity.getBody();
        }
        if (data == null) {
            String message = "Response data type must be [? extends java.util.List] or ResponseEntity<? extends java.util.List>";
            LOGGER.error(message);
            throw new IllegalArgumentException(message);
        }
        if (CollectionUtils.isEmpty(data)) {
            excelExporter.fillTitle();
            return;
        }
        if (data.size() > 1000000)
            throw new CommonException("export.too-many-data");
        excelExporter.fillSheet(data);
    }
}
